#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <unistd.h>
#include <limits.h>
#include <time.h>
#include <math.h>
#include "util.h"

/* CONSTANTS */
#define WINDOW_TITLE "Test 11 - Music Visualizer"
#define DEFAULT_SONG "song.mp3"
/* Command line flags defined here */
#define NUMFLAGS 2
static const char flags[NUMFLAGS] = {'d', 'h'};
static int flagsfound[NUMFLAGS] = {0, 0};

static GLfloat colormap8[8][3] = {
    {1.0, 0.0, 0.0},
    {1.0, 0.65, 0.0},
    {1.0, 1.0, 0.0},
    {0.0, 1.0, 0.0},
    {0.0, 0.0, 1.0},
    {0.31, 0.0, 0.32},
    {0.95, 0.32, 0.95},
    {0.1, 0.5, 0.2}
    };

/* Default window size */
#define WINDOW_WIDTH 500
#define WINDOW_HEIGHT 500
/* Initial camera settings */
#define DEFAULT_ORTHO_HALFHEIGHT 120.0
#define DEFAULT_ORTHO_HALFWIDTH 100.0
#define DEFAULT_ORTHO_HALFZ 250.0
#define CAMERA_X 100
#define CAMERA_Y 200
#define CAMERA_Z 500
#define MIN_ZOOM 0.3
#define MAX_ZOOM 2
#define UP_VECTOR_X 0.0
#define UP_VECTOR_Y 1.0
#define UP_VECTOR_Z 0.0

#define SPECLEN 256
#define FREQUENCYBAR_WIDTH 1
#define FREQUENCYBAR_HEIGHT_MAX 300.0
#define FREQUENCYBAR_HEIGHT_MIN 0.1
#define MAX_NUM_SPHERES 256
#define SPH_INIT_POSRADIUS_MIN 20
#define SPH_INIT_POSRADIUS_RAND 70
#define SPHERE_SLICES 50
#define SPHERE_STACKS 50
#define SPHERE_MINRADIUS 12.0
#define SPHERE_RADIUS_DEFAULT 12.0
#define LIGHT_RADIUS_DEFAULT 4.0
#define INIT_NUM_SPHERES 5
#define SPH_ANGLE_STEP 1
#define MAX_NUM_SPARKS 2000
#define SPARK_SPEED_COEFF 5
#define SPARK_SPEED_EXP 2
#define SPARK_LENGTH 1
#define SPARKS_PER_COLLISION 32
#define SPARKS_ANGLE 11.5
#define MAX_SPARK_LIFETIME 200
#define MAX_NUM_NURBS 2000
#define NURBS_TOLERANCE 25.0
#define NURBS_SPEED_COEFF 10.0
#define NURBS_SPEED_EXP 1
#define TRAJECTORY_RADIUS_MIN 100
#define TRAJECTORY_RADIUS_COEFF 400
#define TRAJECTORY_ANGLE_COEFF 30
#define TRAJECTORY_RADIUS_RAND 1
#define TRAJECTORY_SLOPE_RANGE 50
#define TIME_DELTA 0.0005

#define FULLSCREEN 1
#define COLLISION 1
#define SPARKS 1
#define DEFORMATION 1
/* End constant declaration */

using namespace FMOD;
using namespace std;

System  *fmodSystem;
Sound   *fmodSound;
Channel *fmodChannel;

int
    isDebug
/* Now variables */
  , windowWidth = WINDOW_WIDTH
  , windowHeight = WINDOW_HEIGHT
  , lastX, lastY
  , zoomKey, moveKey
  , rotateCameraKey
  , renderKey = 'W'
  , rollKey
  , currentSparksCount = 0
  , sparkHead = 0
  ;
bool
      drawAxes = false
    , drawPlanes = false
    , drawFrequencies = false
    , drawTrajectories = false
    , drawPerspective = true
    , drawBackground = true
    , drawBackgroundLines = true
    , zoomCamera = false
    , moveCamera = false
    , rotateCamera = false
    , rollCamera = false
    , soundPaused = false
    , bLighting = true /* store lighting value */
    , bCullFace = true /* store cull face value */
    , stopTime = false
    , showTarget = true
    , showSparks = true
    , autoRotate = false
    , showPoints = false
    ;
GLfloat
      *specL, *specR
    , aspectratio = 1.0
/* zoom data */
    , zoom = 1.0
/* rotation angle */
    , angle
/* camera position, before zooming */
    , cameraX = CAMERA_X
    , cameraY = CAMERA_Y
    , cameraZ = CAMERA_Z
/* camera movement, horizontal and vertical */
    , moveCameraX = 0, moveCameraY = 0
/* camera rotation, horizontal and vertical */
    , rotateCameraX = 0, rotateCameraY = 0
/* up vector and rolling */
    , upVectorX = UP_VECTOR_X
    , upVectorY = UP_VECTOR_Y
    , upVectorZ = UP_VECTOR_Z
    , rollAlpha = M_PI / 2
    , r, phi, theta
    , radius
    , step = SPH_ANGLE_STEP
    , sphBoundary[3][2] = {
        {-100.0, 100.0},
        {-100.0, 100.0},
        {-100.0, 100.0}
    }
    , globalTime = 0.0
    , sphTime[MAX_NUM_SPHERES+1]
    , sphTarget[MAX_NUM_SPHERES+1][3]
    , sphSlope[MAX_NUM_SPHERES+1][3]
    , sphCurrent[MAX_NUM_SPHERES+1][3]
    , sphColor[MAX_NUM_SPHERES][3]
    , sphCoeff[MAX_NUM_SPHERES+1][3][4]
    , sparksOrigin[MAX_NUM_SPARKS][4]
    , sparksDirection[MAX_NUM_SPARKS][3]
    , sparksRotationAxis[MAX_NUM_SPARKS][3]
    , globalSpeed = TIME_DELTA * 18
    , nurbsControlPoints[MAX_NUM_NURBS][4][4][3]
    , sparksWall[MAX_NUM_SPARKS][3]
    , modelview[16] = {
        1.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0
    }
    , initModelview[16] = {
        1.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0
    }
    ;

GLfloat 
        lightPosition[] = {
          0.0
        , 0.0
        , 0.0
        , 1.0 }
      , sphMaterial[4]
      ;
GLUnurbsObj *nurbs[MAX_NUM_NURBS];

/* Forward declarations of all functions */
/**
 * Display function
 * Called anytime the display needs to be updated
 */
void display();
/**
 * Resize function
 * Called anytime the window is resized
 */
void reshape(int x, int y);
/**
 * Keyboard and mouse functions
 */
void keyboardUp(unsigned char key, int x, int y);
void keyboardAction(unsigned char key, int x, int y);
void mouseMotion(int x, int y);
void mouseClick(int button, int state, int x, int y);
/**
 * Helper function
 * Sets the program flags accordingly
 */
int getAndSetFlags(int argc, char** argv);

/**
 * Camera manipulation functions
 */
void initRotateCoords();
void doRotateCamera(float newrotate);
void doRollCamera(float newroll);

/**
 * FMOD functions
 */
void initFMOD(const char *path);

/**************** Music visualizer functions ****************/
/**
 * Initializes the spheres.
 * Creates initial trajectories and places spheres in the world.
 */
void initSpheres();
/**
 * Generates a trajectory
 */
void generateTrajectory(int i);
/**
 * Increases sphere internal time and
 * moves all spheres and light sources
 */
void moveSpheres();
/**
 * Calculates current coordinates
 * using existing polynomial
 */
GLfloat calculatePolyCoordinate(int i, int j);
/**
 * Calculates polynomial coefficiants
 * These dictate trajectory to next target
 */
void calculatePolynomialCoeffs(int i, int j, GLfloat newslope);
/**
 * Calculates scalar projection of a vector
 * onto an axis
 */
GLfloat projectTargetOnAxis(int i, GLfloat *axis);
/**
 * Normalizes a 3D vector to length 1
 */
void normalize(GLfloat *coordinates);
/**
 * Calculates Euclidean distance between 2 points in 3D
 */
GLfloat distanceBetweenAB(GLfloat *a, GLfloat *b);
/**
 * Calculates the crosss product b and c in a
 */
GLfloat crossProduct(GLfloat *a, GLfloat *b, GLfloat *c);
/**
 * Helper functions for debugging
 */
void dumpSphereTarget(int i);
void dumpSpherePos(int i);
#ifdef SPARKS
/**
 * Create sparks as spheres i & j collide (i, j >= 0)
 * OR as sphere i and the wall collide (s > 0)
 * if , treated as collision with wall
 */
void spark(int i, int j, int s, GLfloat *unit);
#endif
#ifdef DEFORMATION
/**
 * Initializes the control points of the nurb surface for collision index i
 */
void calculateNurbsSurface(int i);
/**
 * Similar to glutSolidSphere(LIGHT_RADIUS_DEFAULT, SPHERE_SLICES, SPHERE_STACKS);
 */
void drawSphere(GLfloat r, int lats, int longs);
#endif

void display() {
    if (!stopTime) {
        globalTime += globalSpeed;
    }
    /* get music frequencies */
    bool playing;
    fmodChannel->getPaused(&playing);
    if (!playing) {
        fmodChannel->getSpectrum(specL, SPECLEN, 0, FMOD_DSP_FFT_WINDOW_BLACKMAN);
        fmodChannel->getSpectrum(specR, SPECLEN, 1, FMOD_DSP_FFT_WINDOW_BLACKMAN);
    }
    else {
        memset(specL, 0, SPECLEN*sizeof(float));
        memset(specR, 0, SPECLEN*sizeof(float));
    }

    /************************* set perspective *************************/
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    if (drawPerspective) {
        gluPerspective(36.0, aspectratio, 20.0, 1500.0);
        /* move camera randomly */
        /*(float newzoom = zoom
            + (1 - rand() % 3) / 200.0;
        if (newzoom > MIN_ZOOM && newzoom < MAX_ZOOM) {
            zoom = newzoom;
        }*/
        if (!rotateCameraKey && autoRotate) {
            rotateCameraKey = 'Y';
            if ((int)(globalTime * 1/globalSpeed) % 10 == 0)
                doRotateCamera(M_PI/90);
            rotateCameraKey = 0;
        }
        gluLookAt((cameraX+moveCameraX)*zoom
            , (cameraY+moveCameraY)*zoom
            , cameraZ*zoom
            , moveCameraX*zoom
            , moveCameraY*zoom
            , 0.0
            , upVectorX
            , upVectorY
            , upVectorZ
        );
    }
    else {
        /* preserve aspect ratio */
        glOrtho(-windowWidth/WINDOW_WIDTH * DEFAULT_ORTHO_HALFWIDTH
              ,  windowWidth/WINDOW_WIDTH * DEFAULT_ORTHO_HALFWIDTH
              , -windowHeight/WINDOW_HEIGHT * DEFAULT_ORTHO_HALFHEIGHT
              ,  windowHeight/WINDOW_HEIGHT * DEFAULT_ORTHO_HALFHEIGHT
              , -DEFAULT_ORTHO_HALFZ
              , DEFAULT_ORTHO_HALFZ);
    }
    /************************* draw the world *************************/
    glMatrixMode(GL_MODELVIEW);

    glLoadIdentity();
    /* clear everything */
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glEnable(GL_DEPTH_TEST);
    /*************** save values and draw unlit objects ***************/
    bLighting = glIsEnabled(GL_LIGHTING);
    glDisable(GL_LIGHTING);
    bCullFace = glIsEnabled(GL_CULL_FACE);
    glDisable(GL_CULL_FACE);

    /* draw axes */
    if (drawAxes) {
        /* x axis */
        glColor3f(1.0, 0.0, 0.0);
        glBegin(GL_LINES);
            glVertex3f(0.0,0.0,0.0);
            glVertex3f(50.0,0.0,0.0);
        glEnd();
        /* y axis */
        glColor3f(0.0, 1.0, 0.0);
        glBegin(GL_LINES);
            glVertex3f(0.0,0.0,0.0);
            glVertex3f(0.0,50.0,0.0);
        glEnd();
        /* z axis */
        glColor3f(0.0, 0.0, 1.0);
        glBegin(GL_LINES);
            glVertex3f(0.0,0.0,0.0);
            glVertex3f(0.0,0.0,50.0);
        glEnd();
    }
    /* draw planes */
    if (drawPlanes) {
        /* XY plane */
        glColor3f(0.8, 0.8, 1.0);
        if (renderKey == 'W') glBegin(GL_LINE_LOOP);
        else glBegin(GL_POLYGON);
            glVertex3f(-50.0,-50.0,0.0);
            glVertex3f(-50.0,50.0,0.0);
            glVertex3f(50.0,50.0,0.0);
            glVertex3f(50.0,-50.0,0.0);
        glEnd();
        /* XZ plane */
        glColor3f(0.8, 1.0, 0.8);
        if (renderKey == 'W') glBegin(GL_LINE_LOOP);
        else glBegin(GL_POLYGON);
            glVertex3f(-50.0,0.0,-50.0);
            glVertex3f(-50.0,0.0,50.0);
            glVertex3f(50.0,0.0,50.0);
            glVertex3f(50.0,0.0,-50.0);
        glEnd();
        /* YZ plane */
        glColor3f(1.0, 0.8, 0.8);
        if (renderKey == 'W') glBegin(GL_LINE_LOOP);
        else glBegin(GL_POLYGON);
            glVertex3f(0.0,-50.0,-50.0);
            glVertex3f(0.0,50.0,-50.0);
            glVertex3f(0.0,50.0,50.0);
            glVertex3f(0.0,-50.0,50.0);
        glEnd();
    }
    /* draw musical frequencies */
    if (drawFrequencies) {
        int i;
        GLfloat freqHeight;
        for (int j=0; j<SPECLEN; j+=1) {
            i = SPECLEN-1 - j;

            freqHeight = (FREQUENCYBAR_HEIGHT_MAX-FREQUENCYBAR_HEIGHT_MIN) * specL[i] + FREQUENCYBAR_HEIGHT_MIN;
            glBegin(GL_LINE_LOOP);
                glColor4f( 0.6-10*specL[i], 0.2-5*specL[i], 0.5+6*specL[i], 0.4 );
                glVertex3f(-SPECLEN+FREQUENCYBAR_WIDTH*j    ,  freqHeight, 0.0);
                glVertex3f(-SPECLEN+FREQUENCYBAR_WIDTH*(j+1),  freqHeight, 0.0);
                glVertex3f(-SPECLEN+FREQUENCYBAR_WIDTH*(j+1), -freqHeight, 0.0);
                glVertex3f(-SPECLEN+FREQUENCYBAR_WIDTH*j    , -freqHeight, 0.0);
            glEnd();

            freqHeight = (FREQUENCYBAR_HEIGHT_MAX-FREQUENCYBAR_HEIGHT_MIN) * specR[i] + FREQUENCYBAR_HEIGHT_MIN;
            glBegin(GL_LINE_LOOP);
                glColor4f( 0.6-10*specR[i], 0.2-5*specR[i], 0.5+6*specR[i], 0.4 );
                glVertex3f(SPECLEN-FREQUENCYBAR_WIDTH*j    ,  freqHeight, 0.0);
                glVertex3f(SPECLEN-FREQUENCYBAR_WIDTH*(j+1),  freqHeight, 0.0);
                glVertex3f(SPECLEN-FREQUENCYBAR_WIDTH*(j+1), -freqHeight, 0.0);
                glVertex3f(SPECLEN-FREQUENCYBAR_WIDTH*j    , -freqHeight, 0.0);
            glEnd();
        }
    }
    /* draw trajectories of spheres */
    if (drawTrajectories) {
        glPushMatrix();
        for (int j=0; j<INIT_NUM_SPHERES; j++) {
            glBegin(GL_LINES);
                glColor4f(colormap8[j%8][0], colormap8[j%8][1], colormap8[j%8][2], 1.0 );
                glVertex3f(sphCurrent[j][0], sphCurrent[j][1], sphCurrent[j][2]);
                GLfloat line[3];
                line[0] = sphTarget[j][0] - sphCurrent[j][0];
                line[1] = sphTarget[j][1] - sphCurrent[j][1];
                line[2] = sphTarget[j][2] - sphCurrent[j][2];
                if (!showTarget) {
                    normalize(line);
                    for (int k=0; k<3; k++) {
                        line[k] *=  SPHERE_RADIUS_DEFAULT * 6;
                    }
                }
                glVertex3f(sphCurrent[j][0] + line[0]
                         , sphCurrent[j][1] + line[1]
                         , sphCurrent[j][2] + line[2]
                         );
            glEnd();
        }
        glPopMatrix();
    }

    /* Background Lines -- Used for debugging */
    if (drawBackgroundLines) {
        GLfloat insideBy = 1.0;
        glColor3f(1.0f, 1.0f, 1.0f);
        /* BACK */
        glBegin(GL_LINE_STRIP);
        /* 1, 2, 3, 4 */
        glVertex3f(sphBoundary[0][0]+insideBy, sphBoundary[1][0]+insideBy, sphBoundary[2][0]+insideBy);
        glVertex3f(sphBoundary[0][1]-insideBy, sphBoundary[1][0]+insideBy, sphBoundary[2][0]+insideBy);
        glVertex3f(sphBoundary[0][1]-insideBy, sphBoundary[1][1]-insideBy, sphBoundary[2][0]+insideBy);
        glVertex3f(sphBoundary[0][0]+insideBy, sphBoundary[1][1]-insideBy, sphBoundary[2][0]+insideBy);
        /* 1, 5, 6, 4, 1 */
        glVertex3f(sphBoundary[0][0]+insideBy, sphBoundary[1][0]+insideBy, sphBoundary[2][0]+insideBy);
        glVertex3f(sphBoundary[0][0]+insideBy, sphBoundary[1][0]+insideBy, sphBoundary[2][1]-insideBy);
        glVertex3f(sphBoundary[0][0]+insideBy, sphBoundary[1][1]-insideBy, sphBoundary[2][1]-insideBy);
        glVertex3f(sphBoundary[0][0]+insideBy, sphBoundary[1][1]-insideBy, sphBoundary[2][0]+insideBy);
        glVertex3f(sphBoundary[0][0]+insideBy, sphBoundary[1][0]+insideBy, sphBoundary[2][0]+insideBy);
        /* 2, 7, 8, 3 */
        glVertex3f(sphBoundary[0][1]-insideBy, sphBoundary[1][0]+insideBy, sphBoundary[2][0]+insideBy);
        glVertex3f(sphBoundary[0][1]-insideBy, sphBoundary[1][0]+insideBy, sphBoundary[2][1]-insideBy);
        glVertex3f(sphBoundary[0][1]-insideBy, sphBoundary[1][1]-insideBy, sphBoundary[2][1]-insideBy);
        glVertex3f(sphBoundary[0][1]-insideBy, sphBoundary[1][1]-insideBy, sphBoundary[2][0]+insideBy);
        /* 8, 6, 5, 7 */
        glVertex3f(sphBoundary[0][1]-insideBy, sphBoundary[1][1]-insideBy, sphBoundary[2][1]-insideBy);
        glVertex3f(sphBoundary[0][0]+insideBy, sphBoundary[1][1]-insideBy, sphBoundary[2][1]-insideBy);
        glVertex3f(sphBoundary[0][0]+insideBy, sphBoundary[1][0]+insideBy, sphBoundary[2][1]-insideBy);
        glVertex3f(sphBoundary[0][1]-insideBy, sphBoundary[1][0]+insideBy, sphBoundary[2][1]-insideBy);
        glEnd();
    }

    /* light source */
    glPushMatrix();
        lightPosition[0] = sphCurrent[INIT_NUM_SPHERES][0];
        lightPosition[1] = sphCurrent[INIT_NUM_SPHERES][1];
        lightPosition[2] = sphCurrent[INIT_NUM_SPHERES][2];
        glLightfv(GL_LIGHT0, GL_POSITION, lightPosition);
        glTranslatef(sphCurrent[INIT_NUM_SPHERES][0]
                   , sphCurrent[INIT_NUM_SPHERES][1]
                   , sphCurrent[INIT_NUM_SPHERES][2]);
        glutSolidSphere(LIGHT_RADIUS_DEFAULT, SPHERE_SLICES, SPHERE_STACKS);
    glPopMatrix();

    #ifdef SPARKS
    /* sparks */
    if (showSparks) {
        if (currentSparksCount) {
            for (int k=sparkHead; k<currentSparksCount; k++) {
                if (sparksOrigin[k][3] + MAX_SPARK_LIFETIME * globalSpeed <= globalTime) {
                    if (k == sparkHead) sparkHead++;
                    continue;
                }
            glPushMatrix();
                GLfloat
                    r = pow(SPARK_SPEED_COEFF * (globalTime - sparksOrigin[k][3]), SPARK_SPEED_EXP)
                  , sx = sparksDirection[k][0] * r
                  , sy = sparksDirection[k][1] * r
                  , sz = sparksDirection[k][2] * r
                  ;
                glTranslatef(sparksOrigin[k][0], sparksOrigin[k][1], sparksOrigin[k][2]);
                for (int l=0; l<SPARKS_PER_COLLISION; l++) {
                    glRotatef(SPARKS_ANGLE, sparksRotationAxis[k][0], sparksRotationAxis[k][1], sparksRotationAxis[k][2]);
                    glColor4f(colormap8[l%8][0], colormap8[l%8][1], colormap8[l%8][2], 0.5);
                    glBegin(GL_POINTS);
                        glVertex3f(sx, sy, sz);
                    glEnd();
                }
            glPopMatrix();
            }
        }
    }
    #endif
    #ifdef DEFORMATION
    if (showPoints) {
        glColor3f(1.0, 1.0, 0.0);
        glBegin(GL_POINTS);
        for (int i=0; i<4; i++) {
            for (int j=0; j<4; j++) {
            glVertex3f(
                nurbsControlPoints[0][i][j][0]
              , nurbsControlPoints[0][i][j][1]
              , nurbsControlPoints[0][i][j][2]);
            }
        }
        glEnd();
    }
    #endif
    glLoadIdentity();
    /************************ draw lit objects ************************/
    if (bLighting)
        glEnable(GL_LIGHTING);
    if (bCullFace)
        glEnable(GL_CULL_FACE);
    /* Draw Background Cube */
    #ifdef DEFORMATION
    GLfloat knots[8] = {0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0};
    glColor4f(0.1, 0.1, 0.7, 0.5);
    sphMaterial[0] = 0.1;
    sphMaterial[1] = 0.1;
    sphMaterial[2] = 0.7;
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, sphMaterial);
    if (currentSparksCount) {
        for (int k=sparkHead; k<currentSparksCount; k++) {
            if (!sparksWall[k][0]) continue;
            if (sparksOrigin[k][3] + MAX_SPARK_LIFETIME * globalSpeed <= globalTime) {
                if (k == sparkHead) sparkHead++;
                continue;
            }
        glPushMatrix();
            glTranslatef(sparksOrigin[k][0], sparksOrigin[k][1], sparksOrigin[k][2]);
            calculateNurbsSurface(k);
            gluBeginSurface(nurbs[k]);
            gluNurbsSurface(nurbs[k],
                            8, knots, 8, knots,
                            4 * 3, 3, &nurbsControlPoints[k][0][0][0],
                            4, 4, GL_MAP2_VERTEX_3);
            gluEndSurface(nurbs[k]);
        glPopMatrix();
        }
    }

    #endif
    if(drawBackground) {
        glColor4f(0.1, 0.1, 0.7, 0.5);
        sphMaterial[0] = 0.1;
        sphMaterial[1] = 0.1;
        sphMaterial[2] = 0.7;
        glMaterialfv(GL_FRONT, GL_AMBIENT, sphMaterial);
        glBegin(GL_POLYGON);
        /* FRONT */
        glNormal3f(0, 0, 1);
        glVertex3f(sphBoundary[0][0], sphBoundary[1][1], sphBoundary[2][1]);
        glNormal3f(0, 0, 1);
        glVertex3f(sphBoundary[0][1], sphBoundary[1][1], sphBoundary[2][1]);
        glNormal3f(0, 0, 1);
        glVertex3f(sphBoundary[0][1], sphBoundary[1][0], sphBoundary[2][1]);
        glNormal3f(0, 0, 1);
        glVertex3f(sphBoundary[0][0], sphBoundary[1][0], sphBoundary[2][1]);
        glEnd();
        glBegin(GL_POLYGON);

        /* BACK */
        glNormal3f(0, 0, -1);
        glVertex3f(sphBoundary[0][0], sphBoundary[1][0], sphBoundary[2][0]);
        glNormal3f(0, 0, -1);
        glVertex3f(sphBoundary[0][1], sphBoundary[1][0], sphBoundary[2][0]);
        glNormal3f(0, 0, -1);
        glVertex3f(sphBoundary[0][1], sphBoundary[1][1], sphBoundary[2][0]);
        glNormal3f(0, 0, -1);
        glVertex3f(sphBoundary[0][0], sphBoundary[1][1], sphBoundary[2][0]);
        glEnd();
        glBegin(GL_POLYGON);

        /* RIGHT */
        glNormal3f(1, 0, 0);
        glVertex3f(sphBoundary[0][1], sphBoundary[1][0], sphBoundary[2][1]);
        glNormal3f(1, 0, 0);
        glVertex3f(sphBoundary[0][1], sphBoundary[1][1], sphBoundary[2][1]);
        glNormal3f(1, 0, 0);
        glVertex3f(sphBoundary[0][1], sphBoundary[1][1], sphBoundary[2][0]);
        glNormal3f(1, 0, 0);
        glVertex3f(sphBoundary[0][1], sphBoundary[1][0], sphBoundary[2][0]);
        glEnd();
        glBegin(GL_POLYGON);

        /* TOP */
        glNormal3f(0, 1, 0);
        glVertex3f(sphBoundary[0][0], sphBoundary[1][1], sphBoundary[2][1]);
        glNormal3f(0, 1, 0);
        glVertex3f(sphBoundary[0][0], sphBoundary[1][1], sphBoundary[2][0]);
        glNormal3f(0, 1, 0);
        glVertex3f(sphBoundary[0][1], sphBoundary[1][1], sphBoundary[2][0]);
        glNormal3f(0, 1, 0);
        glVertex3f(sphBoundary[0][1], sphBoundary[1][1], sphBoundary[2][1]);
        glEnd();
        glBegin(GL_POLYGON);

        /* LEFT */
        glNormal3f(-1, 0, 0);
        glVertex3f(sphBoundary[0][0], sphBoundary[1][0], sphBoundary[2][1]);
        glNormal3f(-1, 0, 0);
        glVertex3f(sphBoundary[0][0], sphBoundary[1][0], sphBoundary[2][0]);
        glNormal3f(-1, 0, 0);
        glVertex3f(sphBoundary[0][0], sphBoundary[1][1], sphBoundary[2][0]);
        glNormal3f(-1, 0, 0);
        glVertex3f(sphBoundary[0][0], sphBoundary[1][1], sphBoundary[2][1]);
        glEnd();
        glBegin(GL_POLYGON);

        /* BOTTOM */
        glNormal3f(0, -1, 0);
        glVertex3f(sphBoundary[0][0], sphBoundary[1][0], sphBoundary[2][0]);
        glNormal3f(0, -1, 0);
        glVertex3f(sphBoundary[0][0], sphBoundary[1][0], sphBoundary[2][1]);
        glNormal3f(0, -1, 0);
        glVertex3f(sphBoundary[0][1], sphBoundary[1][0], sphBoundary[2][1]);
        glNormal3f(0, -1, 0);
        glVertex3f(sphBoundary[0][1], sphBoundary[1][0], sphBoundary[2][0]);
        glEnd();
    }

    /* draw spheres */
    radius = SPHERE_MINRADIUS + 5*specR[0] + 5*specL[0];
    for (int j=0; j<INIT_NUM_SPHERES; j++) {
        glPushMatrix();
            glColor4f(sphColor[j][0]-10*specL[j], sphColor[j][1]-5*specR[j], sphColor[j][2]+6*specL[j], 1.0);
            sphMaterial[0] = sphColor[j][0]-specL[j];
            sphMaterial[1] = sphColor[j][1]-specR[j];
            sphMaterial[2] = sphColor[j][2]+specL[j];
            glMaterialfv(GL_FRONT, GL_DIFFUSE, sphMaterial);
            sphMaterial[0] /= 10;
            sphMaterial[1] /= 10;
            sphMaterial[2] /= 10;
            glMaterialfv(GL_FRONT, GL_SPECULAR, sphMaterial);
            glMaterialfv(GL_FRONT, GL_AMBIENT, sphMaterial);
            glTranslatef(sphCurrent[j][0], sphCurrent[j][1], sphCurrent[j][2]);
            #ifdef DEFORMATION
            drawSphere(radius, SPHERE_SLICES, SPHERE_STACKS);
            #else
            glutSolidSphere(radius, SPHERE_SLICES, SPHERE_STACKS);
            #endif
        glPopMatrix();
    }
    glutSwapBuffers();
    if (stopTime) {
        glutIdleFunc(NULL);
    }
    moveSpheres();
}

/* Reshape function for when window is reshaped */
void reshape(int x, int y) {
    windowWidth = x;
    windowHeight = y;
    aspectratio = (GLfloat)x/(GLfloat)y;
    glViewport (0, 0, (GLsizei) windowWidth, (GLsizei) windowHeight);
    /* Done, now just call display() */
    display();
}

/* main function */
int main(int argc, char **argv) {
    srand (time(NULL));
    int newargc = getAndSetFlags(argc, argv);
    if (newargc<2) {
        initFMOD(DEFAULT_SONG);
    }
    else {
        initFMOD(argv[argc-1]);
    }
    fmodChannel->setPaused(false);

    /* Initialize glut */
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
    glutInitWindowSize(windowWidth, windowHeight);
    glutInitWindowPosition(100, 100);
    glutCreateWindow(WINDOW_TITLE);

    /* Clear the screen color to black */
    glClearColor(0.0, 0.0, 0.0, 0.0);
    glViewport(0, 0, (GLsizei) windowWidth, (GLsizei) windowHeight);
    glShadeModel(GL_SMOOTH);
    glCullFace(GL_BACK);
    glFrontFace(GL_CCW);
    if (bCullFace) glEnable(GL_CULL_FACE);
    glEnable(GL_LINE_SMOOTH);
    glEnable(GL_POINT_SMOOTH);
    glEnable(GL_AUTO_NORMAL);
    glPointSize(2.0);
    if (bLighting) {
        glEnable(GL_LIGHTING);
        glEnable(GL_LIGHT0);
        /* Create light components */
        GLfloat ambientLight[] = { 0.2f, 0.2f, 0.2f, 1.0f };
        GLfloat diffuseLight[] = { 0.8f, 0.8f, 0.8, 1.0f };
        GLfloat specularLight[] = { 0.5f, 0.5f, 0.5f, 0.5f };
        /* Assign created components to GL_LIGHT0 */
        glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
        glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);
        glLightfv(GL_LIGHT0, GL_SPECULAR, specularLight);

        glEnable(GL_DEPTH_TEST);
    }
    glutSetKeyRepeat(GLUT_KEY_REPEAT_OFF);

    /* Set glut function calls */
    glutDisplayFunc(display);
    glutIdleFunc(display);
    glutReshapeFunc(reshape);
    glutKeyboardFunc(keyboardAction);
    glutKeyboardUpFunc(keyboardUp);
    glutMouseFunc(mouseClick);
    glutMotionFunc(mouseMotion);
    #ifdef FULLSCREEN
        glutFullScreen();
    #endif
    initRotateCoords();

    #ifdef DEFORMATION
    for (int i=0; i<MAX_NUM_NURBS; i++) {
        nurbs[i] = gluNewNurbsRenderer();
        gluNurbsProperty(nurbs[i], GLU_SAMPLING_TOLERANCE, NURBS_TOLERANCE);
        gluNurbsProperty(nurbs[i], GLU_DISPLAY_MODE, GLU_FILL);
    }
    #endif

    /* initialize spheres */
    initSpheres();
    for (int j=0; j<INIT_NUM_SPHERES+1; j++) {
        generateTrajectory(j);
    }
    glutMainLoop();

    fmodSound->release();
    fmodSystem->close();
    fmodSystem->release(); 
    return 0;   /* ISO C requires main to return int. */
}

#ifdef DEFORMATION
void calculateNurbsSurface(int i) {
    int j = sparksWall[i][1];
    for (int u = 0; u < 4; u++) {
        for (int v = 0; v < 4; v++) {
            nurbsControlPoints[i][u][v][(j+1)%3] = 5.0*((GLfloat)u - 1.5);
            nurbsControlPoints[i][u][v][(j+2)%3] = 5.0*((GLfloat)v - 1.5);

            GLfloat
                r = pow(NURBS_SPEED_COEFF * (sparksOrigin[i][3] + MAX_SPARK_LIFETIME/100 - globalTime), NURBS_SPEED_EXP)
                ;
            if ( (u == 1 || u == 2) && (v == 1 || v == 2))
                nurbsControlPoints[i][u][v][j] = r * sparksWall[i][2];
            else
                nurbsControlPoints[i][u][v][j] = 0.0;
        }
    }
}

void drawSphere(GLfloat r, int lats, int longs) {
    int i, j;
    GLfloat lat0, zr0, z0, lat1, z1, zr1, lng, x, y;
    for(i = 0; i <= lats; i++) {
        lat0 = M_PI * (-0.5 + (GLfloat) (i - 1) / lats);
        z0  = sin(lat0);
        zr0 =  cos(lat0);
        lat1 = M_PI * (-0.5 + (GLfloat) i / lats);
        z1 = sin(lat1);
        zr1 = cos(lat1);

        glBegin(GL_QUAD_STRIP);
        for(j = 0; j <= longs; j++) {
            lng = 2 * M_PI * (GLfloat) (j - 1) / longs;
            x = cos(lng);
            y = sin(lng);
            glNormal3f(x * zr1, y * zr1, z1);
            glVertex3f(r*x * zr1, r*y * zr1, r*z1);
            glNormal3f(x * zr0, y * zr0, z0);
            glVertex3f(r*x * zr0, r*y * zr0, r*z0);
        }
        glEnd();
    }
}
#endif

void initSpheres() {
    GLfloat radius, angle1, angle2;
    for (int i=0; i<INIT_NUM_SPHERES; i++) {
        radius = SPH_INIT_POSRADIUS_MIN + rand() % SPH_INIT_POSRADIUS_RAND;
        angle1 = (rand() % 360) * M_PI / 180 ;
        angle2 = (rand() % 360) * M_PI / 180 ;
        sphCurrent[i][0] = radius * sin(angle1) * cos(angle2);
        sphCurrent[i][1] = radius * cos(angle1);
        sphCurrent[i][2] = radius * sin(angle1) * sin(angle2);
        sphColor[i][0] = 0.2 + (rand() % 5000) / 10000.0;
        sphColor[i][1] = 0.2 + (rand() % 5000) / 10000.0;
        sphColor[i][2] = 0.2 + (rand() % 5000) / 10000.0;
        sphSlope[i][0] = TRAJECTORY_SLOPE_RANGE/2 - rand() % TRAJECTORY_SLOPE_RANGE;
        sphSlope[i][1] = TRAJECTORY_SLOPE_RANGE/2 - rand() % TRAJECTORY_SLOPE_RANGE;
        sphSlope[i][2] = TRAJECTORY_SLOPE_RANGE/2 - rand() % TRAJECTORY_SLOPE_RANGE;
        sphTime[i] = 0.0;
    }
    sphMaterial[3] = 1.0;
    sphCurrent[INIT_NUM_SPHERES][0] = sphBoundary[0][0] + rand() % (int)(sphBoundary[0][1]-sphBoundary[0][0]+1);
    sphCurrent[INIT_NUM_SPHERES][0] = sphBoundary[1][0] + rand() % (int)(sphBoundary[1][1]-sphBoundary[1][0]+1);
    sphCurrent[INIT_NUM_SPHERES][2] = sphBoundary[2][1];
    sphSlope[INIT_NUM_SPHERES][0] = TRAJECTORY_SLOPE_RANGE/2 - rand() % TRAJECTORY_SLOPE_RANGE;
    sphSlope[INIT_NUM_SPHERES][1] = TRAJECTORY_SLOPE_RANGE/2 - rand() % TRAJECTORY_SLOPE_RANGE;
    sphSlope[INIT_NUM_SPHERES][2] = TRAJECTORY_SLOPE_RANGE/2 - rand() % TRAJECTORY_SLOPE_RANGE;
}

GLfloat crossProduct(GLfloat *a, GLfloat *b, GLfloat *c) {
    (a)[0] = (b)[1] * (c)[2] - (c)[1] * (b)[2];
    (a)[1] = (b)[2] * (c)[0] - (c)[2] * (b)[0];
    (a)[2] = (b)[0] * (c)[1] - (c)[0] * (b)[1];
}

#ifdef SPARKS
void spark(int i, int j, int s, GLfloat *unit) {
    int pos = currentSparksCount;
    /* sparks arrays are treated as a modified
     * circular queue. sparks are appended to the end,
     * except when sparks at the beginning expire
     */
    if (sparkHead > 0) {
        sparkHead--;
        pos = sparkHead;
    }
    if (s) {
        for (int k=0; k<3; k++) {
            sparksOrigin[pos][k] = sphCurrent[i][k];
        }
        if (s>0) {
            sparksOrigin[pos][j] = sphBoundary[j][1]-2;
        }
        else {
            sparksOrigin[pos][j] = sphBoundary[j][0]+2;
        }
        sparksOrigin[pos][3] = globalTime;
        sparksDirection[pos][(j+1)%3] = 0;
        sparksDirection[pos][(j+2)%3] = 1;
        sparksDirection[pos][j] = 0;
        sparksRotationAxis[pos][(j+1)%3] = 0;
        sparksRotationAxis[pos][(j+2)%3] = 0;
        sparksRotationAxis[pos][j] = 1;
        sparksWall[pos][0] = true;
        sparksWall[pos][1] = j;
        sparksWall[pos][2] = s;
    }
    else {
        for (int k=0; k<3; k++) {
            sparksOrigin[pos][k] = (sphCurrent[i][k] + sphCurrent[j][k])/2;
            sparksDirection[pos][k] = unit[k];
            sparksRotationAxis[pos][k] = (sphCurrent[i][k] - sphCurrent[j][k]);
        }
        normalize(sparksRotationAxis[pos]);
        sparksOrigin[pos][3] = globalTime;
        sparksWall[pos][0] = false;
    }
    currentSparksCount++;
}
#endif

/* from http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=30
 */
void checkCollision(int i, GLfloat *newpos) {
    /* detect collision */
    int l, k, j, c = -1;
    GLfloat
          axis[3]
        , targetvecX[2][3]
        , targetvecY[2][3]
        , projection[2]
        , target1[3], target2[3]
        ;
    /* check collision between sphere and wall */
    bool tooPlus, tooMinus;
    int collideWall=-1, collideSide;
    for (int j=0; j<3; j++) {
        tooMinus = (sphCurrent[i][j] - SPHERE_RADIUS_DEFAULT < sphBoundary[j][0]);
        tooPlus  = (sphCurrent[i][j] + SPHERE_RADIUS_DEFAULT > sphBoundary[j][1]);
        if (tooMinus || tooPlus) {
            /* axis along collision line */
            target1[j] = sphTarget[i][j] - sphCurrent[i][j];
            if (
                /* sphere is close to minus boundary and heading outside box */
                (tooMinus && (target1[j] < 0) && sphCurrent[i][j] > sphBoundary[j][0])
             || (tooPlus  && (target1[j] > 0) && sphCurrent[i][j] < sphBoundary[j][1])
               ) {
                sphTarget[i][j] = sphCurrent[i][j] - target1[j];
                collideWall = j;
                /* negative for left side, positive for right side */
                collideSide = tooPlus-tooMinus;
            }
        }
    }
    if (collideWall>=0) {
        /* calculate cubic poly coeffs */
        sphTime[i] = 0.0;
        for (j=0; j<3; j++) {
            calculatePolynomialCoeffs(i, j, sphSlope[i][j]);
            newpos[j] = calculatePolyCoordinate(i, j);
        }
        #ifdef SPARKS
        spark(i, collideWall, collideSide, NULL);
        #endif
    }
    /* check collision between spheres */
    for (k=i+1; k<INIT_NUM_SPHERES; k++) {
        if (distanceBetweenAB(newpos, sphCurrent[k]) < 2 * SPHERE_RADIUS_DEFAULT) {
            c = k;
            break;
        }
    }

    if (c >= 0) {
        /* already calculated collision in this time step */
        if (sphTime[i] < TIME_DELTA) return;
        if (isDebug) {
            fprintf(stderr, "%f - %d vs %d:", globalTime, i, c);
        }
        /* axis along collision line */
        for (l=0; l<3; l++) {
            axis[l] = sphCurrent[c][l] - sphCurrent[i][l];
        }
        normalize(axis);
        projection[0] = projectTargetOnAxis(i, axis);
        projection[1] = projectTargetOnAxis(c, axis);
        for (l=0; l<3; l++) {
            targetvecX[0][l] = -axis[l] * projection[0];
            targetvecY[0][l] = (sphTarget[i][l] - sphCurrent[i][l]) + targetvecX[0][l];
            targetvecX[1][l] = -axis[l] * projection[1];
            targetvecY[1][l] = (sphTarget[c][l] - sphCurrent[c][l]) + targetvecX[1][l];
            target1[l] = targetvecX[1][l] + targetvecY[0][l];
            target2[l] = targetvecX[0][l] + targetvecY[1][l];
            #ifdef SPARKS
            targetvecY[0][l] += targetvecY[1][l];
            #endif
            /* are they heading the same way? */
            if (target1[l] * target2[l] >= 0) { /* yes */
                /* edge case, need to flip the object "behind" */
                if (axis[l] * target1[l] > 0) {
                    /* sphere[i] is behind sphere[c], flip i */
                    target1[l] = -target1[l];
                }
                else {
                    target2[l] = -target2[l];
                }
            }
            else { /* no */
                /* if they're already going away from each other, don't change anything */
                if ((axis[l] * target1[l] <= 0) && (axis[l] * target2[l] >=0)) {
                    return;
                }
            }
            sphTarget[i][l] = sphCurrent[i][l] + target2[l];
            sphTarget[c][l] = sphCurrent[c][l] + target1[l];
        }
        /* calculate cubic poly coeffs */
        sphTime[i] = 0.0;
        sphTime[c] = 0.0;
        for (j=0; j<3; j++) {
            calculatePolynomialCoeffs(i, j, -sphSlope[i][j]);
            calculatePolynomialCoeffs(c, j, -sphSlope[c][j]);

            newpos[j] = calculatePolyCoordinate(i, j);
            sphCurrent[c][j] = calculatePolyCoordinate(c, j);
        }
        #ifdef SPARKS
        normalize(targetvecY[0]);
        spark(i, c, 0, targetvecY[0]);
        #endif
    }
}

GLfloat projectTargetOnAxis(int i, GLfloat *axis) {
    return axis[0] * (sphTarget[i][0] - sphCurrent[i][0])
         + axis[1] * (sphTarget[i][1] - sphCurrent[i][1])
         + axis[2] * (sphTarget[i][2] - sphCurrent[i][2]);
}

void calculatePolynomialCoeffs(int i, int j, GLfloat newslope) {
    GLfloat lastslope = sphSlope[i][j];
    sphSlope[i][j] = newslope;
    sphCoeff[i][j][0] = sphSlope[i][j] + lastslope + 2 * sphCurrent[i][j] - 2 * sphTarget[i][j];
    sphCoeff[i][j][1] = -sphSlope[i][j] - 2 * lastslope - 3 * sphCurrent[i][j] + 3 * sphTarget[i][j];
    sphCoeff[i][j][2] = lastslope;
    sphCoeff[i][j][3] = sphCurrent[i][j];
}

GLfloat calculatePolyCoordinate(int i, int j) {
    return sphCoeff[i][j][0] * pow(sphTime[i], 3)
         + sphCoeff[i][j][1] * sphTime[i] * sphTime[i]
         + sphCoeff[i][j][2] * sphTime[i]
         + sphCoeff[i][j][3];
}

void moveSpheres() {
    if (stopTime) return;
    GLfloat newpos[3];
    int j;
    for (int i=0; i<INIT_NUM_SPHERES; i++) {
        sphTime[i] += globalSpeed;
        if (sphTime[i] >= 1) {
            sphTime[i] = 0;
            /*dumpSpherePos(i);*/
            generateTrajectory(i);
            /*dumpSphereTarget(i);*/
        }
        for (j=0; j<3; j++) {
            newpos[j] = calculatePolyCoordinate(i, j);
        }
        #ifdef COLLISION
            checkCollision(i, newpos);
        #endif
        for (j=0; j<3; j++) {
            sphCurrent[i][j] = newpos[j];
        }
    }
    /* light sources */
    for (int i=INIT_NUM_SPHERES; i<INIT_NUM_SPHERES+1; i++) {
        sphTime[i] += globalSpeed;
        if (sphTime[i] >= 1) {
            sphTime[i] = 0;
            generateTrajectory(i);
        }
        for (j=0; j<2; j++) {
            sphCurrent[i][j] = calculatePolyCoordinate(i, j);
        }
        sphCurrent[i][2] = sphBoundary[2][1];
    }
}

void generateTrajectory(int i) {
    GLfloat
        radius = 0
      , angle1 = 0, angle2 = 0
      ;
    /* select target */
    if (specL[i] || specR[i]) {
        radius = TRAJECTORY_RADIUS_MIN + specL[i] * TRAJECTORY_RADIUS_COEFF +  rand() % TRAJECTORY_RADIUS_RAND;
        angle1 = (rand() % 360 + specL[i] * TRAJECTORY_ANGLE_COEFF) * M_PI / 180 ;
        angle2 = (rand() % 360 + specL[i] * TRAJECTORY_ANGLE_COEFF) * M_PI / 180 ;
        sphTarget[i][0] = sphCurrent[i][0] + radius * sin(angle1) * cos(angle2);
        sphTarget[i][1] = sphCurrent[i][1] + radius * cos(angle1);
        sphTarget[i][2] = sphCurrent[i][2] + radius * sin(angle1) * sin(angle2);
        for(int j=0; j<3; j++) {
            while (sphTarget[i][j] < sphBoundary[j][0])
                sphTarget[i][j] += radius;
            while (sphTarget[i][j] > sphBoundary[j][1])
                sphTarget[i][j] -= radius;
        };
        /* sphere n+1 is light source in front of other spheres */
        if (i==INIT_NUM_SPHERES) {
            sphTarget[i][2] = sphBoundary[2][1];
        }
        /* calculate cubic poly coeffs */
        for (int j=0; j<3; j++) {
            calculatePolynomialCoeffs(i, j, TRAJECTORY_SLOPE_RANGE/2 - rand() % TRAJECTORY_SLOPE_RANGE);
        }
    }
    else {
        sphTarget[i][0] = sphCurrent[i][0];
        sphTarget[i][1] = sphCurrent[i][1];
        sphTarget[i][2] = sphCurrent[i][2];
        for (int j=0; j<3; j++) {
            sphCoeff[i][j][0] = 0;
            sphCoeff[i][j][1] = 0;
            sphCoeff[i][j][2] = 0;
            sphCoeff[i][j][3] = sphCurrent[i][j];
        }
    }
}


void dumpSphereTarget(int i) {
    fprintf(stderr, "tar: x:%f ||| y:%f ||| z:%f \n"
        , sphTarget[i][0]
        , sphTarget[i][1]
        , sphTarget[i][2]
        );
}

void dumpSpherePos(int i) {
    fprintf(stderr, "pos: x:%f ||| y:%f ||| z:%f \n"
        , sphCurrent[i][0]
        , sphCurrent[i][1]
        , sphCurrent[i][2]
        );
}

void normalize(GLfloat *coordinates) {
    GLfloat magnitude = sqrt(
        coordinates[0] * coordinates[0]
      + coordinates[1] * coordinates[1]
      + coordinates[2] * coordinates[2]
      );
    coordinates[0] /= magnitude;
    coordinates[1] /= magnitude;
    coordinates[2] /= magnitude;
}

GLfloat distanceBetweenAB(GLfloat *a, GLfloat *b) {
    GLfloat
        distX = a[0] - b[0]
      , distY = a[1] - b[1]
      , distZ = a[2] - b[2]
      ;

    return sqrt(
                distX * distX
              + distY * distY
              + distZ * distZ
           );
}

/* initializes coordinates for rotating camera
 * used only for pitch and yaw
 * run once -- see main()
 */
void initRotateCoords() {
    r = sqrt(cameraX*cameraX
            + cameraY*cameraY
            + cameraZ*cameraZ);
    phi = acos(cameraY/r);
    GLfloat S = sqrt(cameraX*cameraX
            + cameraZ*cameraZ);
    theta = asin(cameraZ/S);
}

/* rotates camera in spherical/polar coordinates
 * see http://www.math.montana.edu/frankw/ccp/multiworld/multipleIVP/spherical/body.htm
 */
void doRotateCamera(float newrotate) {
    switch (rotateCameraKey) {
        case 'P':
            phi += newrotate;
            break;
        case 'Y':
            theta += newrotate;
            break;
    }
    GLfloat sinphi = sin(phi);
    if (sinphi < 0)
        sinphi = -sinphi;
    cameraX = r * sinphi * cos(theta);
    cameraZ = r * sinphi * sin(theta);
    cameraY = r * cos(phi);
}

/* rolls camera in 2D polar coordinates (z held constant)
 * upVector assumed already normalized
 */
void doRollCamera(float newroll) {
    rollAlpha += newroll;
    upVectorX = cos(rollAlpha);
    upVectorY = sin(rollAlpha);
}

/* This gets called when the mouse is pressed and moving */
void mouseMotion(int x, int y) {
    if (zoomCamera) {
        float newzoom = zoom
            + ((float)y-(float)lastY) / 200.0;
        lastY = y;
        if (newzoom > MIN_ZOOM && newzoom < MAX_ZOOM) {
            zoom = newzoom;
            glutPostRedisplay();
        }
    }
    else if (moveCamera) {
        int newmove = x-lastX;
        lastX = x;
        switch (moveKey) {
            case 'H':
                moveCameraX += newmove;
                break;
            case 'V':
                moveCameraY += newmove;
                break;
        }
        glutPostRedisplay();
    }
    else if (rotateCamera) {
        float newrotate = x-lastX;
        lastX = x;
        if (newrotate > 0) {
            newrotate = M_PI / 90;
        }
        else {
            newrotate = - M_PI / 90;
        }
        doRotateCamera(newrotate);
        glutPostRedisplay();
    }
    else if (rollCamera) {
        float newroll = x-lastX;
        lastX = x;
        if (newroll > 0) {
            newroll = M_PI / 90;
        }
        else {
            newroll = - M_PI / 90;
        }
        doRollCamera(newroll);
        glutPostRedisplay();
    }
}

/* Takes action when the mouse is clicked.
 * Actions include: rotation, scaling
 * Setting flag for deletion (when ctrl+click is pressed)
 */
void mouseClick(int button, int state, int x, int y) {
    if (state == GLUT_DOWN) {
        /* scaling is middle button */
        if ((zoomKey) && button == GLUT_MIDDLE_BUTTON) {
            zoomCamera = true;
            lastY = y;
        }
        else if ((moveKey) && button == GLUT_RIGHT_BUTTON) {
            moveCamera = true;
            lastX = x;
        }
        else if ((rotateCameraKey) && button == GLUT_LEFT_BUTTON) {
            rotateCamera = true;
            lastX = x;
        }
        else if ((rollKey) && button == GLUT_LEFT_BUTTON) {
            rollCamera = true;
            lastX = x;
        }
        return;
    }
    if (state == GLUT_UP) {
        /* unset all variables on mouseup */
        zoomCamera = false;
        moveCamera = false;
        rotateCamera = false;
        rollCamera = false;
    }
}

/* resets keys; used in conjunction with
 * glutSetKeyRepeat(GLUT_KEY_REPEAT_OFF) -- see main()
 */
void keyboardUp(unsigned char key, int x, int y) {
    zoomKey = 0;
    moveKey = 0;
    rotateCameraKey = 0;
    rollKey = 0;
}

void initFMOD(const char *path) {
    System_Create(&fmodSystem);
    fmodSystem->init(1, FMOD_INIT_NORMAL, 0);
    fmodSystem->createSound(path
              , FMOD_SOFTWARE | FMOD_2D | FMOD_CREATESTREAM
              , 0, &fmodSound);
    fmodSystem->playSound(FMOD_CHANNEL_FREE
              , fmodSound, true, &fmodChannel);
    specL = (float*)malloc(SPECLEN*sizeof(float));
    specR = (float*)malloc(SPECLEN*sizeof(float));
    unsigned int len;
    fmodChannel->setVolume(1);
    fmodSound->getLength(&len, FMOD_TIMEUNIT_MS);
    fmodChannel->setPosition((int)(len*0.0), FMOD_TIMEUNIT_MS);
}

/* Get the flags set from command line and set
 * corresponding variables.
 */
int getAndSetFlags(int argc, char** argv) {
    int i, j;
    if (argc >= 2) {
        for (j = 0; j < NUMFLAGS; j++) {
            for (i = 1; i < argc; i++) {
                if (!flagsfound[j]
                    && strchr(argv[i], '-') 
                    && strchr(argv[i], flags[j])) {
                    flagsfound[j] = 1;
                    if (flags[j] == 'd') {
                        isDebug = 1;
                        argc--;
                    }
                    else if (flags[j] == 'h') {
    /* --------------- HELP SECTION --------------- */
    printf("Usage: %s [-d(debug)|-h(help)] <musicfile.[mp3|flac]>\n", argv[0]);
    printf("Instructions (CaSe SeNsItIvE!):\n");
    printf("CAMERA MANIPULATION:\n");
    printf("- press and hold M, then drag *middle* mouse *vertically* to zoom camera\n");
    printf("- press and hold H or V, then drag *right* mouse *horizontally* to move camera horizontally or vertically\n");
    printf("- press and hold R, P or Y, then drag *left* mouse *horizontally* to do a roll, pitch or yaw of the camera\n");
    printf("- press r to reset camera\n");
    printf("OTHER OPTIONS:\n");
    printf("- press <ESC> to exit\n");
    printf("- press <space> to pause/unpause\n");
    printf("- press p to *toggle* perspective or parallel projection\n");
    printf("- press q to *toggle* musical frequencies\n");
    printf("- press f to *toggle* trajectories of spheres\n");
    printf("- press d to *toggle* between showing target or direction of trajectory (must have above option enabled)\n");
    printf("- press b to *toggle* background cube\n");
    printf("- press B to *toggle* background cube lines\n");
    printf("- press 1-9 to *change* speed\n");
    printf("- press - or = to decrease or increase speed, respectively\n");
    printf("- press w to *toggle* origin axes\n");
    printf("- press e to *toggle* origin planes\n");
    /* --------------- /HELP SECTION --------------- */
                        exit(0);
                    }
                }
            }
        }
    }
    return argc;
}

/* Takes action when a keyboard key is pressed
 */
void keyboardAction(unsigned char key, int x, int y)
{
    switch(key) {
        case 27:
            if (isDebug) {
                fprintf(stderr, "Exiting program, ESC key pressed.\n");
            }
            exit(0);
        case 'r':
            cameraX = CAMERA_X; cameraY = CAMERA_Y; cameraZ = CAMERA_Z;
            zoom = 1.0; moveCameraX = 0; moveCameraY = 0;
            upVectorX = UP_VECTOR_X; upVectorY = UP_VECTOR_Y; upVectorZ = UP_VECTOR_Z;
            for (int i = 0; i<16; i++) {
                modelview[i] = initModelview[i];
            }
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            glutPostRedisplay();
            break;
        case 'q':
            drawFrequencies = !drawFrequencies;
            glutPostRedisplay();
            break;
        case 'f':
            drawTrajectories = !drawTrajectories;
            glutPostRedisplay();
            break;
        case 'd':
            showTarget = !showTarget;
            glutPostRedisplay();
            break;
        #ifdef SPARKS
        case 's':
            showSparks = !showSparks;
            glutPostRedisplay();
            break;
        #endif
        case 'w':
            drawAxes = !drawAxes;
            glutPostRedisplay();
            break;
        case 'c':
            showPoints = !showPoints;
            glutPostRedisplay();
            break;
        case 'a':
            autoRotate = !autoRotate;
            glutPostRedisplay();
            break;
        case 'e':
            drawPlanes = !drawPlanes;
            glutPostRedisplay();
            break;
        case 'p':
            drawPerspective = !drawPerspective;
            glutPostRedisplay();
            break;
        case 'b':
            drawBackground = !drawBackground;
            glutPostRedisplay();
            break;
        case 'B':
            drawBackgroundLines = !drawBackgroundLines;
            glutPostRedisplay();
            break;
        case 'M':
            zoomKey = key;
            break;
        case 'H':
        case 'V':
            moveKey = key;
            break;
        case 'P':
        case 'Y':
            rotateCameraKey = key;
            break;
        case 'W':
        case 'C':
        case 'G':
            renderKey = key;
            glutPostRedisplay();
            break;
        case 'R':
            rollKey = key;
            break;
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
            globalSpeed = (key - (int)'0') * 2 * TIME_DELTA;
            break;
        case '-':
            if (globalSpeed > TIME_DELTA)
                globalSpeed -= TIME_DELTA;
            break;
        case '=':
            globalSpeed += TIME_DELTA;
            break;
        case ' ':
            soundPaused = !soundPaused;
            stopTime = !stopTime;
            fmodChannel->setPaused(soundPaused);
            if (!soundPaused) glutIdleFunc(display);
                else glutIdleFunc(NULL);
            break;
        default:
            fprintf(stderr, "Unreckognized key: %d\n", key);
    }
}
